package eval

import (
	"showen/lex"
	"showen/num"
	"showen/obj"
	"showen/parse"
	"testing"
)

func TestApply(t *testing.T) {
	env := obj.NewEnvironment()
	RegisterBuiltinProcedures(env)

	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "apply with + and list",
			input:    "(apply + '(1 2 3))",
			expected: "6",
		},
		{
			name:     "apply with cons and list",
			input:    "(apply cons '(1 2))",
			expected: "(1 . 2)",
		},
		{
			name:     "apply with extra arguments",
			input:    "(apply + 1 2 '(3 4))",
			expected: "10",
		},
		{
			name:     "apply with empty list",
			input:    "(apply + '())",
			expected: "0",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lexer := lex.NewLexer(tt.input)
			tokens := lexer.ReadTokens()
			parser := parse.NewParser(tokens)
			expr, err := parser.ParseExpression()
			if err != nil {
				t.Fatalf("ParseExpression() error = %v", err)
			}

			result, err := EvalExpression(expr, env)
			if err != nil {
				t.Fatalf("EvalExpression() error = %v", err)
			}

			if result.Inspect() != tt.expected {
				t.Errorf("EvalExpression() = %v, want %v", result.Inspect(), tt.expected)
			}
		})
	}
}

func TestApplyErrors(t *testing.T) {
	env := obj.NewEnvironment()
	RegisterBuiltinProcedures(env)

	errorTests := []struct {
		name        string
		input       string
		expectedErr string
	}{
		{
			name:        "apply with too few arguments",
			input:       "(apply)",
			expectedErr: "apply requires at least 2 arguments",
		},
		{
			name:        "apply with one argument",
			input:       "(apply +)",
			expectedErr: "apply requires at least 2 arguments",
		},
		{
			name:        "apply with non-procedure",
			input:       "(apply 42 '(1 2 3))",
			expectedErr: "first argument to apply must be a procedure",
		},
		{
			name:        "apply with non-list last argument",
			input:       "(apply + 42)",
			expectedErr: "last argument to apply must be a list",
		},
	}

	for _, tt := range errorTests {
		t.Run(tt.name, func(t *testing.T) {
			lexer := lex.NewLexer(tt.input)
			tokens := lexer.ReadTokens()
			parser := parse.NewParser(tokens)
			expr, err := parser.ParseExpression()
			if err != nil {
				t.Fatalf("ParseExpression() error = %v", err)
			}

			result, err := EvalExpression(expr, env)
			if err != nil {
				// Error occurred during evaluation, check if it matches expected error
				if err.Error() != tt.expectedErr {
					t.Errorf("EvalExpression() error = %v, want %v", err.Error(), tt.expectedErr)
				}
				return
			}

			// No error during evaluation, check if result is an error object
			if result.Type() != obj.ERROR_OBJ {
				t.Fatalf("Expected error object, got %v", result.Type())
			}

			errorObj := result.(*Error)
			if errorObj.Message != tt.expectedErr {
				t.Errorf("EvalExpression() error = %v, want %v", errorObj.Message, tt.expectedErr)
			}
		})
	}
}

func TestMap(t *testing.T) {
	env := obj.NewEnvironment()
	RegisterBuiltinProcedures(env)

	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "map with car and list of pairs",
			input:    "(map car '((1 2) (3 4) (5 6)))",
			expected: "(1 3 5)",
		},
		{
			name:     "map with + and two lists",
			input:    "(map + '(1 2 3) '(4 5 6))",
			expected: "(5 7 9)",
		},
		{
			name:     "map with lambda",
			input:    "(map (lambda (x) (* x x)) '(1 2 3 4))",
			expected: "(1 4 9 16)",
		},
		{
			name:     "map with empty list",
			input:    "(map car '())",
			expected: "()",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lexer := lex.NewLexer(tt.input)
			tokens := lexer.ReadTokens()
			parser := parse.NewParser(tokens)
			expr, err := parser.ParseExpression()
			if err != nil {
				t.Fatalf("ParseExpression() error = %v", err)
			}

			result, err := EvalExpression(expr, env)
			if err != nil {
				t.Fatalf("EvalExpression() error = %v", err)
			}

			if result.Inspect() != tt.expected {
				t.Errorf("EvalExpression() = %v, want %v", result.Inspect(), tt.expected)
			}
		})
	}
}

func TestMapErrors(t *testing.T) {
	env := obj.NewEnvironment()
	RegisterBuiltinProcedures(env)

	errorTests := []struct {
		name        string
		input       string
		expectedErr string
	}{
		{
			name:        "map with too few arguments",
			input:       "(map)",
			expectedErr: "map requires at least 2 arguments",
		},
		{
			name:        "map with one argument",
			input:       "(map car)",
			expectedErr: "map requires at least 2 arguments",
		},
		{
			name:        "map with non-procedure",
			input:       "(map 42 '(1 2 3))",
			expectedErr: "first argument to map must be a procedure",
		},
		{
			name:        "map with non-list argument",
			input:       "(map car 42)",
			expectedErr: "arguments to map must be lists",
		},
		{
			name:        "map with lists of different lengths",
			input:       "(map + '(1 2 3) '(4 5))",
			expectedErr: "all lists must have the same length",
		},
	}

	for _, tt := range errorTests {
		t.Run(tt.name, func(t *testing.T) {
			lexer := lex.NewLexer(tt.input)
			tokens := lexer.ReadTokens()
			parser := parse.NewParser(tokens)
			expr, err := parser.ParseExpression()
			if err != nil {
				t.Fatalf("ParseExpression() error = %v", err)
			}

			result, err := EvalExpression(expr, env)
			if err != nil {
				// Error occurred during evaluation, check if it matches expected error
				if err.Error() != tt.expectedErr {
					t.Errorf("EvalExpression() error = %v, want %v", err.Error(), tt.expectedErr)
				}
				return
			}

			// No error during evaluation, check if result is an error object
			if result.Type() != obj.ERROR_OBJ {
				t.Fatalf("Expected error object, got %v", result.Type())
			}

			errorObj := result.(*Error)
			if errorObj.Message != tt.expectedErr {
				t.Errorf("EvalExpression() error = %v, want %v", errorObj.Message, tt.expectedErr)
			}
		})
	}
}

func TestForEach(t *testing.T) {
	env := obj.NewEnvironment()
	RegisterBuiltinProcedures(env)

	// Test that for-each returns nil (which should be represented as "")
	t.Run("for-each with empty list", func(t *testing.T) {
		input := "(for-each display '())"
		lexer := lex.NewLexer(input)
		tokens := lexer.ReadTokens()
		parser := parse.NewParser(tokens)
		expr, err := parser.ParseExpression()
		if err != nil {
			t.Fatalf("ParseExpression() error = %v", err)
		}

		result, err := EvalExpression(expr, env)
		if err != nil {
			t.Fatalf("EvalExpression() error = %v", err)
		}

		// for-each should return nil, which has an empty string representation
		if result.Inspect() != "" {
			t.Errorf("EvalExpression() = %v, want empty string", result.Inspect())
		}

		// Also check the type is NULL_OBJ
		if result.Type() != obj.NULL_OBJ {
			t.Errorf("Expected NULL_OBJ, got %v", result.Type())
		}
	})
}

func TestForEachErrors(t *testing.T) {
	env := obj.NewEnvironment()
	RegisterBuiltinProcedures(env)

	errorTests := []struct {
		name        string
		input       string
		expectedErr string
	}{
		{
			name:        "for-each with too few arguments",
			input:       "(for-each)",
			expectedErr: "for-each requires at least 2 arguments",
		},
		{
			name:        "for-each with one argument",
			input:       "(for-each display)",
			expectedErr: "for-each requires at least 2 arguments",
		},
		{
			name:        "for-each with non-procedure",
			input:       "(for-each 42 '(1 2 3))",
			expectedErr: "first argument to for-each must be a procedure",
		},
		{
			name:        "for-each with non-list argument",
			input:       "(for-each display 42)",
			expectedErr: "arguments to for-each must be lists",
		},
		{
			name:        "for-each with lists of different lengths",
			input:       "(for-each cons '(1 2 3) '(4 5))",
			expectedErr: "all lists must have the same length",
		},
	}

	for _, tt := range errorTests {
		t.Run(tt.name, func(t *testing.T) {
			lexer := lex.NewLexer(tt.input)
			tokens := lexer.ReadTokens()
			parser := parse.NewParser(tokens)
			expr, err := parser.ParseExpression()
			if err != nil {
				t.Fatalf("ParseExpression() error = %v", err)
			}

			result, err := EvalExpression(expr, env)
			if err != nil {
				// Error occurred during evaluation, check if it matches expected error
				if err.Error() != tt.expectedErr {
					t.Errorf("EvalExpression() error = %v, want %v", err.Error(), tt.expectedErr)
				}
				return
			}

			// No error during evaluation, check if result is an error object
			if result.Type() != obj.ERROR_OBJ {
				t.Fatalf("Expected error object, got %v", result.Type())
			}

			errorObj := result.(*Error)
			if errorObj.Message != tt.expectedErr {
				t.Errorf("EvalExpression() error = %v, want %v", errorObj.Message, tt.expectedErr)
			}
		})
	}
}

// Helper functions for testing
func createList(objects ...obj.Object) *obj.PairObject {
	if len(objects) == 0 {
		return nil
	}

	// Create list by consing elements from right to left
	var result obj.Object = obj.EMPTY_LIST
	for i := len(objects) - 1; i >= 0; i-- {
		result = &obj.PairObject{Car: objects[i], Cdr: result}
	}

	return result.(*obj.PairObject)
}

func createNumber(value int64) *obj.NumberObject {
	return &obj.NumberObject{Value: num.Integer(value)}
}

func createBoolean(value bool) *obj.BooleanObject {
	return &obj.BooleanObject{Value: value}
}
